"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SourceFile = void 0;
const core_1 = require("@mikro-orm/core");
class SourceFile {
    constructor(meta, namingStrategy, platform, esmImport) {
        this.meta = meta;
        this.namingStrategy = namingStrategy;
        this.platform = platform;
        this.esmImport = esmImport;
        this.coreImports = new Set();
        this.entityImports = new Set();
    }
    generate() {
        this.coreImports.add('Entity');
        let ret = `@Entity(${this.getCollectionDecl()})\n`;
        this.meta.indexes.forEach(index => {
            this.coreImports.add('Index');
            const properties = core_1.Utils.asArray(index.properties).map(prop => `'${prop}'`);
            ret += `@Index({ name: '${index.name}', properties: [${properties.join(', ')}] })\n`;
        });
        this.meta.uniques.forEach(index => {
            this.coreImports.add('Unique');
            const properties = core_1.Utils.asArray(index.properties).map(prop => `'${prop}'`);
            ret += `@Unique({ name: '${index.name}', properties: [${properties.join(', ')}] })\n`;
        });
        ret += `export class ${this.meta.className} {`;
        const enumDefinitions = [];
        const optionalProperties = [];
        const primaryProps = [];
        let classBody = '\n';
        Object.values(this.meta.properties).forEach(prop => {
            const decorator = this.getPropertyDecorator(prop, 2);
            const definition = this.getPropertyDefinition(prop, 2);
            if (!classBody.endsWith('\n\n')) {
                classBody += '\n';
            }
            classBody += decorator;
            classBody += definition;
            classBody += '\n';
            if (prop.enum) {
                const enumClassName = this.namingStrategy.getClassName(this.meta.collection + '_' + prop.fieldNames[0], '_');
                enumDefinitions.push(this.getEnumClassDefinition(enumClassName, prop.items, 2));
            }
            if (!prop.nullable && typeof prop.default !== 'undefined') {
                optionalProperties.push(prop);
            }
            if (prop.primary && (!['id', '_id', 'uuid'].includes(prop.name) || this.meta.compositePK)) {
                primaryProps.push(prop);
            }
        });
        if (primaryProps.length > 0) {
            this.coreImports.add('PrimaryKeyType');
            this.coreImports.add('PrimaryKeyProp');
            const findType = (p) => p.reference === core_1.ReferenceType.SCALAR ? p.type : this.platform.getMappedType(p.columnTypes[0]).compareAsType();
            const primaryPropNames = primaryProps.map(prop => `'${prop.name}'`);
            const primaryPropTypes = primaryProps.map(prop => findType(prop));
            ret += `\n\n${' '.repeat(2)}[PrimaryKeyProp]?: ${primaryPropNames.join(' | ')};`;
            if (primaryProps.length > 1) {
                ret += `\n\n${' '.repeat(2)}[PrimaryKeyType]?: [${primaryPropTypes.join(', ')}];`;
            }
            else {
                ret += `\n\n${' '.repeat(2)}[PrimaryKeyType]?: ${primaryPropTypes[0]};`;
            }
        }
        if (optionalProperties.length > 0) {
            this.coreImports.add('OptionalProps');
            const optionalPropertyNames = optionalProperties.map(prop => `'${prop.name}'`).sort();
            ret += `\n\n${' '.repeat(2)}[OptionalProps]?: ${optionalPropertyNames.join(' | ')};`;
        }
        ret += `${classBody}}\n`;
        const imports = [`import { ${([...this.coreImports].sort().join(', '))} } from '@mikro-orm/core';`];
        const entityImportExtension = this.esmImport ? '.js' : '';
        const entityImports = [...this.entityImports].filter(e => e !== this.meta.className);
        entityImports.sort().forEach(entity => {
            imports.push(`import { ${entity} } from './${entity}${entityImportExtension}';`);
        });
        ret = `${imports.join('\n')}\n\n${ret}`;
        if (enumDefinitions.length) {
            ret += '\n' + enumDefinitions.join('\n');
        }
        return ret;
    }
    getBaseName() {
        return this.meta.className + '.ts';
    }
    quote(val) {
        /* istanbul ignore next */
        return val.startsWith(`'`) ? `\`${val}\`` : `'${val}'`;
    }
    getPropertyDefinition(prop, padLeft) {
        const padding = ' '.repeat(padLeft);
        if ([core_1.ReferenceType.ONE_TO_MANY, core_1.ReferenceType.MANY_TO_MANY].includes(prop.reference)) {
            this.coreImports.add('Collection');
            this.entityImports.add(prop.type);
            return `${padding}${prop.name} = new Collection<${prop.type}>(this);\n`;
        }
        // string defaults are usually things like SQL functions, but can be also enums, for that `useDefault` should be true
        const isEnumOrNonStringDefault = prop.enum || typeof prop.default !== 'string';
        const useDefault = prop.default != null && isEnumOrNonStringDefault;
        const optional = prop.nullable ? '?' : (useDefault ? '' : '!');
        if (prop.wrappedReference) {
            this.coreImports.add('Ref');
            this.entityImports.add(prop.type);
            return `${padding}${prop.name}${optional}: Ref<${prop.type}>;\n`;
        }
        const ret = `${prop.name}${optional}: ${prop.type}`;
        if (!useDefault) {
            return `${padding + ret};\n`;
        }
        if (prop.enum && typeof prop.default === 'string') {
            return `${padding}${ret} = ${prop.type}.${prop.default.toUpperCase()};\n`;
        }
        return `${padding}${ret} = ${prop.default};\n`;
    }
    getEnumClassDefinition(enumClassName, enumValues, padLeft) {
        const padding = ' '.repeat(padLeft);
        let ret = `export enum ${enumClassName} {\n`;
        for (const enumValue of enumValues) {
            ret += `${padding}${enumValue.toUpperCase()} = '${enumValue}',\n`;
        }
        ret += '}\n';
        return ret;
    }
    getCollectionDecl() {
        const options = {};
        if (this.meta.collection !== this.namingStrategy.classToTableName(this.meta.className)) {
            options.tableName = this.quote(this.meta.collection);
        }
        if (this.meta.schema && this.meta.schema !== this.platform.getDefaultSchemaName()) {
            options.schema = this.quote(this.meta.schema);
        }
        if (!core_1.Utils.hasObjectKeys(options)) {
            return '';
        }
        return `{ ${Object.entries(options).map(([opt, val]) => `${opt}: ${val}`).join(', ')} }`;
    }
    getPropertyDecorator(prop, padLeft) {
        const padding = ' '.repeat(padLeft);
        const options = {};
        let decorator = this.getDecoratorType(prop);
        this.coreImports.add(decorator.substring(1));
        if (prop.reference === core_1.ReferenceType.MANY_TO_MANY) {
            this.getManyToManyDecoratorOptions(options, prop);
        }
        else if (prop.reference === core_1.ReferenceType.ONE_TO_MANY) {
            this.getOneToManyDecoratorOptions(options, prop);
        }
        else if (prop.reference !== core_1.ReferenceType.SCALAR) {
            this.getForeignKeyDecoratorOptions(options, prop);
        }
        else {
            this.getScalarPropertyDecoratorOptions(options, prop);
        }
        if (prop.enum) {
            options.items = `() => ${prop.type}`;
        }
        this.getCommonDecoratorOptions(options, prop);
        const indexes = this.getPropertyIndexes(prop, options);
        decorator = [...indexes.sort(), decorator].map(d => padding + d).join('\n');
        if (!core_1.Utils.hasObjectKeys(options)) {
            return `${decorator}()\n`;
        }
        return `${decorator}({ ${Object.entries(options).map(([opt, val]) => `${opt}: ${val}`).join(', ')} })\n`;
    }
    getPropertyIndexes(prop, options) {
        if (prop.reference === core_1.ReferenceType.SCALAR) {
            const ret = [];
            if (prop.index) {
                this.coreImports.add('Index');
                ret.push(`@Index({ name: '${prop.index}' })`);
            }
            if (prop.unique) {
                this.coreImports.add('Unique');
                ret.push(`@Unique({ name: '${prop.unique}' })`);
            }
            return ret;
        }
        const processIndex = (type) => {
            if (!prop[type]) {
                return;
            }
            const defaultName = this.platform.getIndexName(this.meta.collection, prop.fieldNames, type);
            options[type] = defaultName === prop[type] ? 'true' : `'${prop[type]}'`;
            const expected = {
                index: this.platform.indexForeignKeys(),
                unique: prop.reference === core_1.ReferenceType.ONE_TO_ONE,
            };
            if (expected[type] && options[type] === 'true') {
                delete options[type];
            }
        };
        processIndex('index');
        processIndex('unique');
        return [];
    }
    getCommonDecoratorOptions(options, prop) {
        if (prop.nullable && !prop.mappedBy) {
            options.nullable = true;
        }
        if (prop.default == null) {
            return;
        }
        if (typeof prop.default !== 'string') {
            options.default = prop.default;
            return;
        }
        if ([`''`, ''].includes(prop.default)) {
            options.default = `''`;
        }
        else if (prop.defaultRaw === this.quote(prop.default)) {
            options.default = this.quote(prop.default);
        }
        else {
            options.defaultRaw = `\`${prop.default}\``;
        }
    }
    getScalarPropertyDecoratorOptions(options, prop) {
        let t = prop.type.toLowerCase();
        if (t === 'date') {
            t = 'datetime';
        }
        if (prop.fieldNames[0] !== this.namingStrategy.propertyToColumnName(prop.name)) {
            options.fieldName = `'${prop.fieldNames[0]}'`;
        }
        // for enum properties, we don't need a column type or the property length
        // in the decorator so return early.
        if (prop.enum) {
            return;
        }
        const mappedType1 = this.platform.getMappedType(t);
        const mappedType2 = this.platform.getMappedType(prop.columnTypes[0]);
        const columnType1 = mappedType1.getColumnType({ ...prop, autoincrement: false }, this.platform);
        const columnType2 = mappedType2.getColumnType({ ...prop, autoincrement: false }, this.platform);
        if (columnType1 !== columnType2 || [mappedType1, mappedType2].some(t => t instanceof core_1.UnknownType)) {
            options.columnType = this.quote(columnType2);
        }
        const assign = (key) => {
            if (prop[key] != null) {
                options[key] = prop[key];
            }
        };
        if (!(mappedType2 instanceof core_1.DateType) && !options.columnType) {
            assign('length');
        }
        // those are already included in the `columnType` in most cases, and when that option is present, they would be ignored anyway
        /* istanbul ignore next */
        if (mappedType2 instanceof core_1.DecimalType && !options.columnType) {
            assign('precision');
            assign('scale');
        }
    }
    getManyToManyDecoratorOptions(options, prop) {
        this.entityImports.add(prop.type);
        options.entity = `() => ${prop.type}`;
        if (prop.mappedBy) {
            options.mappedBy = this.quote(prop.mappedBy);
            return;
        }
        if (prop.pivotTable !== this.namingStrategy.joinTableName(this.meta.collection, prop.type, prop.name)) {
            options.pivotTable = this.quote(prop.pivotTable);
        }
        if (prop.joinColumns.length === 1) {
            options.joinColumn = this.quote(prop.joinColumns[0]);
        }
        else {
            options.joinColumns = `[${prop.joinColumns.map(this.quote).join(', ')}]`;
        }
        if (prop.inverseJoinColumns.length === 1) {
            options.inverseJoinColumn = this.quote(prop.inverseJoinColumns[0]);
        }
        else {
            options.inverseJoinColumns = `[${prop.inverseJoinColumns.map(this.quote).join(', ')}]`;
        }
    }
    getOneToManyDecoratorOptions(options, prop) {
        this.entityImports.add(prop.type);
        options.entity = `() => ${prop.type}`;
        options.mappedBy = this.quote(prop.mappedBy);
    }
    getForeignKeyDecoratorOptions(options, prop) {
        const parts = prop.referencedTableName.split('.', 2);
        const className = this.namingStrategy.getClassName(parts.length > 1 ? parts[1] : parts[0], '_');
        this.entityImports.add(className);
        options.entity = `() => ${className}`;
        if (prop.wrappedReference) {
            options.ref = true;
        }
        if (prop.mappedBy) {
            options.mappedBy = this.quote(prop.mappedBy);
            return;
        }
        if (prop.fieldNames[0] !== this.namingStrategy.joinKeyColumnName(prop.name, prop.referencedColumnNames[0])) {
            options.fieldName = this.quote(prop.fieldNames[0]);
        }
        if (!['no action', 'restrict'].includes(prop.onUpdateIntegrity.toLowerCase())) {
            options.onUpdateIntegrity = this.quote(prop.onUpdateIntegrity);
        }
        if (!['no action', 'restrict'].includes(prop.onDelete.toLowerCase())) {
            options.onDelete = this.quote(prop.onDelete);
        }
        if (prop.primary) {
            options.primary = true;
        }
    }
    getDecoratorType(prop) {
        if (prop.reference === core_1.ReferenceType.ONE_TO_ONE) {
            return '@OneToOne';
        }
        if (prop.reference === core_1.ReferenceType.MANY_TO_ONE) {
            return '@ManyToOne';
        }
        if (prop.reference === core_1.ReferenceType.ONE_TO_MANY) {
            return '@OneToMany';
        }
        if (prop.reference === core_1.ReferenceType.MANY_TO_MANY) {
            return '@ManyToMany';
        }
        if (prop.primary) {
            return '@PrimaryKey';
        }
        if (prop.enum) {
            return '@Enum';
        }
        return '@Property';
    }
}
exports.SourceFile = SourceFile;
